/* 
 * Autores:
 *      Jonathan Lopez  11106
 *      Jorge Martínez  11237
 * 
 * Ultima revision: $Rev: 28 $
 *      Fecha: $Date: 2011-11-13 08:23:56 +0000 (Sun, 13 Nov 2011) $
 * 
 * Archivo: ArchivoCSV.java
 */

package Datos.CSV;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Scanner;
import org.jasypt.util.text.BasicTextEncryptor;

/**
 * Representa a un archivo separado por comas. Toma la primera linea como
 * los nombres de las columnas. El resto de las lineas son leidas truncando
 * los datos que se salgan de la cantidad de columnas establecidas por la
 * primera linea.
 * 
 * @author Jorge
 * @author Jonathan
 */
public final class ArchivoCSV {
    private boolean valido = false;
    
    private String[] nombreColumnas = new String[0];
    private ArrayList<String[]> datos = new ArrayList<String[]>();
    
    private final String nombreArchivo;
    private final boolean encriptar;
    
    // Por el momento se usa una llave publica tempora. Posteriormente
    // se pondra una mas segura.
    private final String llaveTemporal = "llaveTemporal";
    
    /**
     * Abre un archivo de tipo CSV.
     * 
     * @param nombreArchivo Nombre del archivo a abrir.
     */
    public ArchivoCSV(String nombreArchivo, boolean encriptar) {
        this.nombreArchivo = nombreArchivo;
        this.encriptar = encriptar;
        
        BasicTextEncryptor encriptador = new BasicTextEncryptor();
        encriptador.setPassword(llaveTemporal);
        
        // Abrir y leer el archivo.
        try {
            Scanner entrada = new Scanner( new File(nombreArchivo) );    
            while( entrada.hasNextLine() ){
                
                // Desencriptar
                String linea = entrada.nextLine();
                if( encriptar )
                    linea = encriptador.decrypt(linea);
                
                if( nombreColumnas.length == 0 ){
                    // Es la primera linea, por lo que son los nombres de las
                    // columnas
                    nombreColumnas = linea.split( "," );
                } else {
                    // Seguir leyendo cada linea
                    agregarLinea( linea );
                }
            }
            
        } catch (FileNotFoundException ex) {
            return;
        }
        
        // Marcar que la operacion fue exitosa
        valido = true;
    }
    
    /**
     * Crea un nuevo archivo de tipo CSV con las columnas dadas. Si el archivo
     * ya existe en el disco entonces es sobreescrito.
     * 
     * @param nombreArchivo Nombre del archivo a crear.
     * @param nombreColumnas Arreglo con los strings que identifican las columnas
     * de la tabla.
     */
    public ArchivoCSV( String nombreArchivo, String[] nombreColumnas,
            boolean encriptar ){
        this.nombreArchivo = nombreArchivo;
        this.nombreColumnas = nombreColumnas;
        this.encriptar = encriptar;
        
        if( nombreColumnas == null || nombreColumnas.length == 0)
            return;
        
        // Crear el archivo y escribir los nombres de las columnas
        try {
            PrintWriter salida = new PrintWriter( nombreArchivo );
            String primeraLinea = "";
            for (int columnaActual = 0; columnaActual < nombreColumnas.length;
                    columnaActual++) {
                if( columnaActual == 0 )
                    primeraLinea = nombreColumnas[columnaActual];
                else
                    primeraLinea += "," + nombreColumnas[columnaActual];
            }
            
            // Encriptar
            if( encriptar ){
                BasicTextEncryptor encriptador = new BasicTextEncryptor();
                encriptador.setPassword(llaveTemporal);
                primeraLinea = encriptador.encrypt(primeraLinea);
            }
            
            salida.println( primeraLinea );
            salida.close();
        } catch (FileNotFoundException ex) {
            return;
        }
        
        // Marcar que la operacion fue exitosa
        valido = true;
    }
    
    /**
     * Agrega una linea de texto a los datos del CSV. Los campos de la linea
     * de texto deben de estar separados por coma para ser ingresados. Si
     * tiene mas datos que el numero de columnas entonces se truncan los
     * datos extras.
     * 
     * @param linea String con los campos separados por coma.
     * @return Indice de la linea que se escribio, o -1 si ocurrio un error.
     */
    public int agregarLinea( String linea ){
        // Validar
        if( linea == null || linea.isEmpty() )
            return -1;
        
        // Agregar la linea
        return agregarLinea( linea.split(",") );
    }
    
    /**
     * Agrega una linea de datos al CSV. Los datos que sobran son truncados.
     * @param linea Arreglo con los datos de cada campo.
     * @return Indice de la linea que se escribio, o -1 si ocurrio un error.
     */
    public int agregarLinea( String[] linea ){
        // Validar
        if( linea == null || linea.length == 0 )
            return -1;
        
        datos.add( Arrays.copyOf(linea, nombreColumnas.length) );
        return datos.size() - 1;
    }
    
    /**
     * Elimina la linea especificada.
     * @param nLinea Numero de linea a eliminar.
     * @return True si la operacion fue exitosa, false en caso contrario.
     */
    public boolean eliminarLinea( int nLinea ){
        // Validar
        if( !esValido() || nLinea < 0 || nLinea >= datos.size() )
            return false;
        
        // Eliminar la linea
        datos.remove(nLinea);
        return true;
    }
    
    /**
     * Borra las lineas especificadas en el arreglo
     * @param Lineas Indices de las lineas que se borraran
     */
    public void eliminarLineas( Integer[] Lineas ){
        // Validar
        if( Lineas.length == 0 )
            return;
        
        // Agregar todos los indices a un Set para evitar que se repitan
        HashSet<Integer> setLineas = new HashSet<Integer>();
        setLineas.addAll(Arrays.asList(Lineas));
        
        // Ordenar de mayor a menor para evitar que el corrimiento de indices
        // al eliminar una linea afecte el de los demas
        ArrayList<Integer> listaLineas = new ArrayList<Integer>(setLineas);
        Collections.sort(listaLineas, Collections.reverseOrder());
        
        // Eliminar cada linea una por una
        for (Integer lineaBorrar : listaLineas) {
            eliminarLinea(lineaBorrar);
        }
    }
    
    /**
     * Modifica la linea dada con los nuevos datos
     * @param nuevaLinea Nuevos datos con los que se reemplazan los anteriores.
     * Los datos deben de estar separados por coma.
     * @param nLinea Numero de linea a modificar.
     * @return True si la operacion fue exitosa, y false en caso contrario.
     */
    public boolean modificarLinea( String nuevaLinea, int nLinea ){
        // Validar
        if( !esValido() || nLinea < 0 || nLinea >= datos.size() ||
                nuevaLinea == null )
            return false;
        
        return modificarLinea( nuevaLinea.split(","), nLinea );
    }
    
    /**
     * Modifica la linea dada con los nuevos datos
     * @param lineaNueva Nuevos datos con los que se reemplazan los anteriores.
     * @param nLinea Numero de linea a modificar.
     * @return True si la operacion fue exitosa, y false en caso contrario.
     */
    public boolean modificarLinea( String[] nuevaLinea, int nLinea ){
        // Validar
        if( !esValido() || nLinea < 0 || nLinea >= datos.size() ||
                nuevaLinea == null )
            return false;
        
        // Modifivar la linea dada
        datos.set(nLinea, Arrays.copyOf(nuevaLinea, nombreColumnas.length));
        return true;
    }
    
    /**
     * Regresa un arreglo con los campos de la linea dada, o null si la linea
     * no existe.
     * @param nLinea Numero de linea a obtener.
     * @return Arreglo con los datos, o null si la linea no existe.
     */
    public String[] obtenerLinea( int nLinea ){
        // Validar
        if( !esValido() || nLinea < 0 || nLinea >= datos.size() )
            return null;
        
        // Regresar la linea
        return datos.get(nLinea);
    }
    
    /**
     * Busca en todas las lineas del archivo las que tengan el valor dado
     * en la columna dada.
     * @param columnaBuscar Nombre de la columna en la que se buscara.
     * @param valor Valor a buscar en la columna.
     * @return Regresa un arreglo de ints con cero o mas indices de las lineas
     * que cumplen los requisitos de la busqueda.
     */
    public Integer[] buscarLineas( String columnaBuscar, String valor ){
        ArrayList<Integer> lineasEncontradas = new ArrayList<Integer>();
        
        // Buscar el indice de la columna
        int indiceColumna = -1;
        for (int i = 0; i < nombreColumnas.length; i++) {
            String nombreCol = nombreColumnas[i];
            if( nombreCol.equals(columnaBuscar) ){
                indiceColumna = i;
                break;
            }
        }
        if( indiceColumna == -1 )
            return new Integer[0];
        
        // Buscar los datos en la columna
        for (int i = 0; i < obtenerCantidadLineas(); i++) {
            String[] linea = obtenerLinea(i);
            if( linea[indiceColumna].equals(valor) )
                lineasEncontradas.add(i);
        }
        
        return lineasEncontradas.toArray( new Integer[0] );
    }
    
    /**
     * Busca en todas las lineas del archivo las que tengan el valor dado
     * en la columna dada.
     * @param columnaBuscar Nombre de la columna en la que se buscara.
     * @param valor Valor a buscar en la columna.
     * @param rangoLineas Arreglo de int conteniendo el rango de lineas en
     * los cuales buscar.
     * @return Regresa un arreglo de ints con cero o mas indices de las lineas
     * que cumplen los requisitos de la busqueda.
     */
    public Integer[] buscarLineas( String columnaBuscar, String valor,
            Integer[] rangoLineas ){
        // Validar
        if( rangoLineas.length == 0 )
            return new Integer[0];
        
        ArrayList<Integer> lineasEncontradas = new ArrayList<Integer>();
        
        // Buscar el indice de la columna
        int indiceColumna = -1;
        for (int i = 0; i < nombreColumnas.length; i++) {
            String nombreCol = nombreColumnas[i];
            if( nombreCol.equals(columnaBuscar) ){
                indiceColumna = i;
                break;
            }
        }
        if( indiceColumna == -1 )
            return new Integer[0];
        
        // Buscar los datos en la columna, en el rango de lineas
        for (Integer lineaBuscar : rangoLineas) {
            String[] linea = obtenerLinea(lineaBuscar);
            if( linea[indiceColumna].equals(valor) )
                lineasEncontradas.add(lineaBuscar);
        }
        
        return lineasEncontradas.toArray( new Integer[0] );
    }
    
    /**
     * @return Regresa la cantidad de lineas que contiene el archivo.
     */
    public int obtenerCantidadLineas(){
        if( !esValido() )
            return -1;
        else
            return datos.size();
    }
    
    /**
     * Guarda todos los cambios en el archivo
     * @return True si la operacion fue exitosa, false en caso contrario.
     */
    public boolean guardarCambios(){
        // Validar
        if( !esValido() )
            return false;
        
        BasicTextEncryptor encriptador = new BasicTextEncryptor();
        encriptador.setPassword(llaveTemporal);
        
        // Abrir el archivo y escribir los datos en el
        try {
            PrintWriter salida = new PrintWriter( nombreArchivo );
            
            // Agregar los nombres de las columnas
            String linea = convertirLinea( nombreColumnas );
            if( encriptar )
                linea = encriptador.encrypt(linea);
            salida.println( linea );
            
            // Agregar las lineas con los campos
            for (String[] lineaDatos : datos) {
                linea = convertirLinea( lineaDatos );
                
                if( encriptar )
                    linea = encriptador.encrypt(linea);
                
                salida.println( linea );
            }
            salida.close();
            
        } catch (FileNotFoundException ex) {
            return false;
        }
        
        return true;
    }
    
    /**
     * @return True si el archivo fue abierto e interpretado correctamente,
     * false en caso contrario.
     */
    public boolean esValido(){
        return valido;
    }

    /**
     * @return Regresa una representacion String del archivo.
     */
    @Override
    public String toString() {
        if( esValido() ){
            String[][] stringDatos = datos.toArray( new String[0][0] );
                    
            return "ArchivoCSV{" + "valido=" + valido +
                    ", nombreArchivo=" + nombreArchivo +
                    ", nombreColumnas=" + Arrays.toString(nombreColumnas) +
                    ", datos=" + Arrays.deepToString(stringDatos) + '}';
        }
        else
            return "ArchivoCSV{" + "valido=" + valido +
                    ", nombreArchivo=" + nombreArchivo + '}';
    }
    
    /**
     * Convierte un arreglo de Strings en una linea con los campos separados
     * por comas.
     * @param linea Campos a agregar
     * @return Linea con los campos separados por coma.
     */
    private String convertirLinea( String[] linea ){
        String retorno = "";
        for (int i = 0; i < linea.length; i++) {
            if( i == 0 )
                retorno = linea[i];
            else
                retorno += "," + (linea[i] == null ? "" : linea[i]);
        }
        return retorno;
    }
    
    /**
     * Punto de entrada para las pruebas unitarias de ArchivoCSV
     * @param args 
     */
    public static void main( String[] args ){
        // Leer la tabla de usuarios y de servicios
        ArchivoCSV usuarios = new ArchivoCSV("DatosCSV/usuarios.csv", false);
        ArchivoCSV servicios = new ArchivoCSV("DatosCSV/servicios.csv", false );
        ArchivoCSV prueba = new ArchivoCSV( "DatosCSV/prueba.csv",
                new String[]{"col1", "col2", "col3"}, false );
        prueba.agregarLinea("dat1,dat2,dat3,dat4,dat5");
        prueba.agregarLinea("dat1,dat2,dat3,dat4,dat5");
        prueba.agregarLinea("dat1,dat2,dat3,dat4,dat5");
        prueba.agregarLinea("dat1,,dat3");
        prueba.agregarLinea("dato largo, dato únícódé, dato, dato de mas");
        prueba.eliminarLinea(1);
        prueba.modificarLinea("datNuevo1, datoNuevo2, datoNuevo3, datoNuevo4", 0);
        prueba.modificarLinea("datNuevo1", 6);
        prueba.guardarCambios();
        
        System.out.println(usuarios);
        System.out.println(servicios);
        System.out.println(prueba);
    }
}
